package org.libjpopup.sender;

import jcifs.netbios.NbtAddress;
import jpcap.*;
import jpcap.packet.*;

import java.io.*;
import java.util.Arrays;


public class Sender {
    private NetworkInterface networkInterface;
    private NetworkInterfaceAddress networkInterfaceAddress;
    private JpcapCaptor captor;
    private JpcapSender sender;

    public static final byte[] BROADCAST_HWADDR = new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF};
    public static final String SUCCESS = "success";

    private static final String PARAMS_DELIMITER = "\0";
    private static final String RARROW = "~-->~";
    private static final String LARROW = "~<--~";
    private static final int MAX_SHORT_POPUP_LENGTH = 399;
    private static final int MAX_FIRST_LAST_POPUP_LENGTH = MAX_SHORT_POPUP_LENGTH - RARROW.length();
    private static final int MAX_MIDDLE_POPUP_LENGTH = MAX_SHORT_POPUP_LENGTH - RARROW.length() - LARROW.length();

    private static final int MAX_NETBIOS_LEN = 15;
    private static final String ENCODING = "CP866";

    private static final boolean DEBUG = false;

    public static void main(String[] args) throws IOException {
        String result = Sender.sendSingle("libjpopup", "workgroup", "preved!");
    }

    /**
     * Method for popup sending with automatical network device open and close with explicit receiver ip
     *
     * @param src_name sender name
     * @param dst_name receiver name
     * @param msg_text message text
     * @param dst_ip   receiver ip address
     * @return Sender.SUCCESS in case of successfull sending or "error"+cause of error otherwise
     * @throws IOException
     */
    public static String sendSingle(String src_name, String dst_name, String msg_text, String dst_ip) throws IOException {
        Sender sender = new Sender();
        sender.open();
        String result = sender.send(src_name, dst_name, msg_text, dst_ip);
        sender.close();
        return result;
    }

    /**
     * Method for popup sending with automatical network device open and close with receiver ip resolving
     *
     * @param src_name sender name
     * @param dst_name receiver name
     * @param msg_text message text
     * @return Sender.SUCCESS in case of successfull sending or "error"+cause of error otherwise
     * @throws IOException
     */
    public static String sendSingle(String src_name, String dst_name, String msg_text) throws IOException {
        return sendSingle(src_name, dst_name, msg_text, null);
    }


    /**
     * Method for network interface shutdown
     *
     * @throws java.io.IOException in case of unsuccessful network device open attempt
     */
    public void open() throws IOException {
        for (NetworkInterface iface : JpcapCaptor.getDeviceList())
            if (!iface.loopback)
                for (NetworkInterfaceAddress address : iface.addresses)
                    if (address.address != null &&
                            address.address.getHostAddress().startsWith("10.")) {//we're searching for the local(10.x.y.z) address;
                        networkInterfaceAddress = address;
                        networkInterface = iface;
                        captor = JpcapCaptor.openDevice(networkInterface, 1024, false, 3000);
                        captor.setFilter("arp", true);
                        sender = captor.getJpcapSenderInstance();
                        jcifs.Config.setProperty("jcifs.resolveOrder", "BCAST");
                        jcifs.Config.setProperty("jcifs.netbios.baddr", networkInterfaceAddress.broadcast.getHostAddress());
                    }
    }

    /**
     * Method for network interface shutdown
     */
    public void close() {
        sender.close();
        captor.close();
    }

    /**
     * Method for popup send with dst ip resolving
     *
     * @param src_name sender name
     * @param dst_name receiver name
     * @param msg_text message text
     * @return Sender.SUCCESS in case of successfull sending or "error"+cause of error otherwise
     */
    public synchronized String send(String src_name, String dst_name, String msg_text) {
        return send(src_name, dst_name, msg_text, null);
    }

    /**
     * @param src_name sender name
     * @param dst_name receiver name
     * @param msg_text message text
     * @param dst_ip   receiver ip address
     * @return Sender.SUCCESS in case of successfull sending or "error"+cause of error otherwise
     */
    public synchronized String send(String src_name, String dst_name, String msg_text, String dst_ip) {
        try {
            EthernetPacket ethernetPacket = new EthernetPacket();
            ethernetPacket.frametype = EthernetPacket.ETHERTYPE_IP;
            ethernetPacket.src_mac = networkInterface.mac_address;
//            ethernetPacket.src_mac = new byte[]{0, 22, -53, -74, -87, 105};  //00:16:cb:b6:a9:68 <- mac spoofing could be added here

            UDPPacket udpPacket = new UDPPacket(138, 138);

            NbtAddress addr;
            if (dst_ip == null)                         //if destination ip is not set explicitely
                addr = NbtAddress.getByName(dst_name);  // we resolve NbtAddress by name
            else addr = NbtAddress.getByName(dst_ip);   //otherwise - by ip

            if (addr.isGroupAddress()) {                //if address is workgroup addres we need to send it to the broadcast address
                ethernetPacket.dst_mac = BROADCAST_HWADDR;
                udpPacket.setIPv4Parameter(0, false, false, false, 0, false, false, false, 0, 0, 64, IPPacket.IPPROTO_UDP, networkInterfaceAddress.address, networkInterfaceAddress.broadcast);
            } else {
                ethernetPacket.dst_mac = ARP.arp(captor, sender, networkInterface, networkInterfaceAddress, addr.getInetAddress());
                udpPacket.setIPv4Parameter(0, false, false, false, 0, false, false, false, 0, 0, 64, IPPacket.IPPROTO_UDP, networkInterfaceAddress.address, addr.getInetAddress());
            }

            udpPacket.datalink = ethernetPacket;

            byte[][] dataPackets = CreatePackets(src_name, networkInterfaceAddress.address.getAddress(), dst_name, msg_text);

            for (byte[] data : dataPackets) {
                udpPacket.data = data;
                if (DEBUG) System.out.println(Arrays.toString(data));
                else sender.sendPacket(udpPacket);
            }
        } catch (IOException e) {
            return "error: " + e.getCause();
        }
        return SUCCESS;
    }

    private static byte[] CreateSinglePacket(String src_name, byte[] src_ip, String receiver, String text) throws UnsupportedEncodingException {
        //some hard-coded data here; could be generated from scratch but this is not actually needed
        byte b[] = {17, 2, -126, -1, 10, 0, 0, 7, 0, -118, 1, 24, 0, 0, 32, 69, 73, 69, 70, 69, 77, 69, 77, 67, 65, 67, 65, 67, 65, 67, 65, 67, 65, 67, 65, 67, 65, 67, 65, 67, 65, 67, 65, 67, 65, 65, 65, 0, 32, 68, 66, 68, 70, 67, 65, 67, 65, 67, 65, 67, 65, 67, 65, 67, 65, 67, 65, 67, 65, 67, 65, 67, 65, 67, 65, 67, 65, 67, 65, 65, 65, 0, -1, 83, 77, 66, 37, 0, 0, 0, 0, 24, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -2, 0, 0, 0, 0, 17, 0, 0, 124, 0, 2, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 88, 0, 124, 0, 88, 0, 3, 0, 1, 0, 0, 0, 2, 0, -113, 0, 92, 77, 65, 73, 76, 83, 76, 79, 84, 92, 109, 101, 115, 115, 110, 103, 114, 0, 0};
        String popupStringData = src_name + PARAMS_DELIMITER + receiver.toUpperCase() + PARAMS_DELIMITER + text + PARAMS_DELIMITER;
        byte[] mydata = popupStringData.getBytes("Cp866");
        byte[] popuptext = new byte[mydata.length + 170];
        byte[] sourceName = nameToSmbByteArray(src_name);
        byte[] destName = nameToSmbByteArray(receiver);
        byte[] mydataSize = decToByteArray(mydata.length);
        byte[] mydataPlusHeader1Size = decToByteArray(mydata.length + 19);
        byte[] mydataPlusHeader2Size = decToByteArray(mydata.length + 19 + 137);
        byte[] mydataPlusHeader2SizeRevverted = {mydataPlusHeader2Size[1], mydataPlusHeader2Size[0]};
        System.arraycopy(mydataSize, 0, b, 117, 2);
        System.arraycopy(mydataSize, 0, b, 137, 2);
        System.arraycopy(mydataPlusHeader1Size, 0, b, 149, 2);
        System.arraycopy(mydataPlusHeader2SizeRevverted, 0, b, 10, 2);
        System.arraycopy(b, 0, popuptext, 0, 4);
        System.arraycopy(src_ip, 0, popuptext, 4, src_ip.length);
        System.arraycopy(b, 8, popuptext, 8, 6);
        System.arraycopy(sourceName, 0, popuptext, 14, sourceName.length);
        System.arraycopy(destName, 0, popuptext, 48, destName.length);
        System.arraycopy(b, 82, popuptext, 82, 88);
        System.arraycopy(mydata, 0, popuptext, 170, mydata.length);
        return popuptext;
    }

    private static void decToByteArray(int decNum, byte[] bArray, int startIndex) {
        if (decNum < 256) {
            bArray[startIndex] = (byte) decNum;
            bArray[startIndex + 1] = 0;
        } else {
            bArray[startIndex] = (byte) (decNum % 256);
            bArray[startIndex + 1] = (byte) (decNum / 256);
        }
    }


    private static byte[][] CreatePackets(String src_name, byte[] src_ip, String receiver, String text) throws IOException {
        text = text.replace("\r\n", "\n");
        text = text.replace("\r", "");
        text = text.replace("\n", "\r\n");
        int dgm_id = (int) (255 * Math.random());//some special random datagram_id

        int totalLength = (src_name + PARAMS_DELIMITER + receiver + PARAMS_DELIMITER + text + PARAMS_DELIMITER).length();

        if (totalLength <= MAX_SHORT_POPUP_LENGTH) {
            byte[] shortPacket = CreateSinglePacket(src_name, src_ip, receiver, text);
            return new byte[][]{shortPacket};
        } else {                                         //cast to double for proper ceiling; todo: check
            byte[][] packets = new byte[(int) Math.ceil(((double) totalLength - LARROW.length() - RARROW.length()) / MAX_FIRST_LAST_POPUP_LENGTH)][];

            int senderReceiverLength = (src_name + PARAMS_DELIMITER + receiver).length();

            byte[] packetFirst = CreateSinglePacket(src_name, src_ip, receiver, text.substring(0, MAX_FIRST_LAST_POPUP_LENGTH - senderReceiverLength) + RARROW);
            System.arraycopy(decToByteArray(dgm_id++), 0, packetFirst, 2, 2);
            packets[0] = packetFirst;
            text = text.substring(MAX_FIRST_LAST_POPUP_LENGTH - senderReceiverLength);

            for (int i = 1; i < packets.length - 1; i++) {
                byte[] packetMiddle = CreateSinglePacket(src_name, src_ip, receiver, LARROW + text.substring(0, MAX_MIDDLE_POPUP_LENGTH - senderReceiverLength) + RARROW);
                System.arraycopy(decToByteArray(dgm_id++), 0, packetMiddle, 2, 2);
                packets[i] = packetMiddle;
                text = text.substring(MAX_MIDDLE_POPUP_LENGTH - senderReceiverLength);
            }

            byte[] packetLast = CreateSinglePacket(src_name, src_ip, receiver, LARROW + text);
            System.arraycopy(decToByteArray(dgm_id), 0, packetLast, 2, 2);
            packets[packets.length - 1] = packetLast;
            return packets;
        }
    }

    private static byte[] decToByteArray(int decNum) {//todo: inline
        return new byte[]{(byte) (decNum % 256), (byte) (decNum / 256)};
    }

    private static byte[] nameToSmbByteArray(String name) throws UnsupportedEncodingException {//todo: re-write
        for (int i = name.length(); i < MAX_NETBIOS_LEN; i++) name += " ";
        name = name.substring(0, MAX_NETBIOS_LEN);
        byte[] bbb = name.toUpperCase().getBytes(ENCODING);//todo: use Arrays.fill


        byte[] byteName = new byte[34];

        for (int i = 0; i < bbb.length; i++) {
            byte b = bbb[i];
            byteName[i * 2 + 1] = (byte) (((b >> 4) & 0x000F) + 'A');
            byteName[(i * 2) + 2] = (byte) ((b & 0x000F) + 'A');
        }
        byteName[0] = 32;
        byteName[31] = 65;
        byteName[32] = 65;
        byteName[33] = '\0';
        return byteName;
    }
}